using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;

namespace Flight
{
    public class FlightGame : Microsoft.Xna.Framework.Game
    {
        public static FlightGame game;

        public static float GRAVITY;

        Settings settings;

        public GraphicsDeviceManager graphics;
        public SpriteBatch spriteBatch;

        SpriteFont font;

        Dictionary<string, Model> models = new Dictionary<string, Model>();
        Dictionary<string, SoundEffect> sounds = new Dictionary<string, SoundEffect>();

        bool full_screen;

        bool controlMode;
        const bool KEYBOARD_CONTROL = true;
        const bool MOUSE_CONTROL = false;

        List<Aircraft> aircrafts;
        public Aircraft playerAircraft;
        public Person playerPerson;
        float d;
        float power = .5f;

        Texture2D shadowImage, airportImage, dirtTileImage;
        Model controlTowerModel;

        const float TERRAIN_SIZE = 100000;
        const float TERRAIN_CELL_SIZE = 100;
        const int TERRAIN_RESOLUTION = (int)(TERRAIN_SIZE / TERRAIN_CELL_SIZE);

        Airport airport;

        public Vector3 cameraLoc;

        public int width { get { return Window.ClientBounds.Width; } }
        public int height { get { return Window.ClientBounds.Height; } }

        public FlightGame(string[] args)
        {
            FlightGame.game = this;

            string dir = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
            Settings.setBasePath(System.IO.Path.Combine(dir, "data"));

            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
        }

        protected override void Initialize()
        {
            settings = new Settings("settings.txt");
            GRAVITY = settings.get<float>("gravity");

            graphics.IsFullScreen = full_screen = settings.has("fullscreen");
            if (full_screen)
            {
                graphics.PreferredBackBufferWidth = GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Width;
                graphics.PreferredBackBufferHeight = GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Height;
            }
            else
            {
                graphics.PreferredBackBufferWidth = settings.get<int>("width");
                graphics.PreferredBackBufferHeight = settings.get<int>("height");
            }
            graphics.ApplyChanges();

            Window.Title = "Flight";

            controlMode = MOUSE_CONTROL == (settings.get<string>("control-mode") == "mouse");

            airport = Airport.fromSaveFileText(
                System.IO.File.ReadAllLines(
                    System.IO.Path.Combine(Settings.BasePath, "airports", "airport.apt")));

            settings.listUnusedSettings();
            
            // set up quads
            basicEffect = new BasicEffect(graphics.GraphicsDevice);
            basicEffect.EnableDefaultLighting();
            basicEffect.TextureEnabled = true;
            basicEffect.SpecularPower = float.MaxValue;

            vertexDeclaration = vertexDeclaration ?? new VertexDeclaration(new VertexElement[] {
                new VertexElement(0, VertexElementFormat.Vector3, VertexElementUsage.Position, 0),
                new VertexElement(12, VertexElementFormat.Vector3, VertexElementUsage.Normal, 0),
                new VertexElement(24, VertexElementFormat.Vector2, VertexElementUsage.TextureCoordinate, 0)
            });

            Mouse.SetPosition(width / 2, height / 2);

            base.Initialize();
        }

        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);

            font = Content.Load<SpriteFont>("font");
            shadowImage = Texture2D.FromStream(GraphicsDevice, 
                System.IO.File.OpenRead(
                    System.IO.Path.Combine(Settings.BasePath, "images", "shadow.png")));
            airportImage = Texture2D.FromStream(GraphicsDevice,
                System.IO.File.OpenRead(
                    System.IO.Path.Combine(Settings.BasePath, "images", "airport.png")));
            dirtTileImage = Texture2D.FromStream(GraphicsDevice,
                System.IO.File.OpenRead(
                    System.IO.Path.Combine(Settings.BasePath, "images", "dirt-tile.png")));
            //controlTowerModel = Content.Load<Model>("control-tower");
            
            playerAircraft = Aircraft.load(settings.get<string>("aircraft"), new Vector3(), 0, 0);
            d = playerAircraft.VIEW_DISTANCE;

            aircrafts = new List<Aircraft>();
            aircrafts.Add(playerAircraft);
            // row of parked airplanes
            for (int i = 0; i < 17; i++)
            {
                aircrafts.Add(Aircraft.load(
                    settings.get<string>("aircraft"),
                    playerAircraft.MODEL,
                    new Vector3(-70, i * 20, 0),
                    0, 0));
            }
        }

        public Model getModel(string model)
        {
            model = model.Remove(model.IndexOf('.'));
            if (models.ContainsKey(model))
                return models[model];
            else return models[model] = Content.Load<Model>(model);
        }
        public SoundEffect getSound(string sound)
        {
            sound = sound.Remove(sound.IndexOf('.'));
            if (sounds.ContainsKey(sound))
                return sounds[sound];
            else return sounds[sound] = Content.Load<SoundEffect>(sound);
        }

        protected override void UnloadContent()
        {
            // Unload any non ContentManager content here
        }

        int prevMouseWheel = 0;
        protected override void Update(GameTime gameTime)
        {
            // Allows the game to exit
            if (Keyboard.GetState().IsKeyDown(Keys.Escape))
                this.Exit();

            int mouseWheel = Mouse.GetState().ScrollWheelValue;
            int diff = (mouseWheel - prevMouseWheel) / 120; // 120 per notch
            if (diff != 0) 
                onMouseWheel(diff);
            prevMouseWheel = mouseWheel;

            // TODO: Add your update logic here
            double dt = gameTime.ElapsedGameTime.TotalSeconds;

            if (Key.isPressed(Keys.Z)) d -= 30 * (float)dt;
            if (Key.isPressed(Keys.X)) d += 30 * (float)dt;

            if (Key.isPressed(Keys.R) || (Mouse.GetState().LeftButton == ButtonState.Pressed))
                power = MathHelper.Clamp(power + 0.5f * (float)dt, 0, 1);
            if (Key.isPressed(Keys.F) || (Mouse.GetState().RightButton == ButtonState.Pressed))
                power = MathHelper.Clamp(power - 0.5f * (float)dt, 0, 1);

            // 'i' changes _i_nput mode
            if (Key.wasPressed(Keys.I)) controlMode ^= true;

            if (Key.wasPressed(Keys.A) && Key.isPressed(Keys.LeftShift))
            {
                if (playerAircraft.getAutopilot() is AirplaneAutopilot)
                    ((AirplaneAutopilot)playerAircraft.getAutopilot()).ap_alt ^= true;
                //else if (playerAircraft.getAutopilot() is HelicopterAutopilot)
                //    ((HelicopterAutopilot)playerAircraft.getAutopilot()).ap_alt ^= true;
            }
            if (Key.wasPressed(Keys.S) && Key.isPressed(Keys.LeftShift))
            {
                if (playerAircraft.getAutopilot() is AirplaneAutopilot)
                    ((AirplaneAutopilot)playerAircraft.getAutopilot()).ap_speed ^= true;
                //else if (playerAircraft.getAutopilot() is HelicopterAutopilot)
                //    ((HelicopterAutopilot)playerAircraft.getAutopilot()).ap_speed ^= true;
            }
            if (Key.wasPressed(Keys.H) && Key.isPressed(Keys.LeftShift))
            {
                if (playerAircraft.getAutopilot() is AirplaneAutopilot)
                    ((AirplaneAutopilot)playerAircraft.getAutopilot()).ap_heading ^= true;
                //else if (playerAircraft.getAutopilot() is HelicopterAutopilot)
                //    ((HelicopterAutopilot)playerAircraft.getAutopilot()).ap_heading ^= true;
            }
            if (Key.wasPressed(Keys.A)) ap_setting = 'a';
            if (Key.wasPressed(Keys.S)) ap_setting = 's';
            if (Key.wasPressed(Keys.H)) ap_setting = 'h';
            if (Key.wasPressed(Keys.V)) ap_setting = 'v';
            if (Key.isPressed(Keys.OemPlus))
            {
                if (playerAircraft.getAutopilot() is AirplaneAutopilot)
                {
                    AirplaneAutopilot ap = (AirplaneAutopilot)playerAircraft.getAutopilot();
                    if (ap_setting == 'a') ap.targetAlt += 100 * (float)dt;
                    else if (ap_setting == 's') ap.targetSpeed += 25 * (float)dt;
                    else if (ap_setting == 'h') ap.targetHeading += 90 * (float)dt;
                    else if (ap_setting == 'v') ap.targetVSpeed += 100f / 60 * (float)dt;
                }
                else if (playerAircraft.getAutopilot() is HelicopterAutopilot)
                {
                    //				HelicopterAutopilot ap = (HelicopterAutopilot)playerAircraft.getAutopilot();
                    //				if (ap_setting == 'a') ap.setTargetAlt(ap.getTargetAlt() + 100 / frameRate);
                    //				else if (ap_setting == 's') ap.setTargetSpeed(ap.getTargetSpeed() + 25 / frameRate);
                    //				else if (ap_setting == 'h') ap.setTargetHeading(ap.getTargetHeading() + 90 / frameRate);
                    //				else if (ap_setting == 'v') ap.setTargetVSpeed(ap.getTargetVSpeed() + 100f / 60 / frameRate);
                }
            }
            else if (Key.isPressed(Keys.OemMinus))
            {
                if (playerAircraft.getAutopilot() is AirplaneAutopilot)
                {
                    AirplaneAutopilot ap = (AirplaneAutopilot)playerAircraft.getAutopilot();
                    if (ap_setting == 'a') ap.targetAlt -= 100 * (float)dt;
                    else if (ap_setting == 's') ap.targetSpeed -= 25 * (float)dt;
                    else if (ap_setting == 'h') ap.targetHeading -= 90 * (float)dt;
                    else if (ap_setting == 'v') ap.targetVSpeed -= 100f / 60 * (float)dt;
                }
                else if (playerAircraft.getAutopilot() is HelicopterAutopilot)
                {
                    //				HelicopterAutopilot ap = (HelicopterAutopilot)playerAircraft.getAutopilot();
                    //				if (ap_setting == 'a') ap.setTargetAlt(ap.getTargetAlt() - 100 / frameRate);
                    //				else if (ap_setting == 's') ap.setTargetSpeed(ap.getTargetSpeed() - 25 / frameRate);
                    //				else if (ap_setting == 'h') ap.setTargetHeading(ap.getTargetHeading() - 90 / frameRate);
                    //				else if (ap_setting == 'v') ap.setTargetVSpeed(ap.getTargetVSpeed() - 100f / 60 / frameRate);
                }
            }

            if (Key.wasPressed(Keys.E))
            {
                if (playerAircraft != null)
                {
                    // evacuate airplane!
                    playerPerson = new Person(
                        playerAircraft.getLoc(), playerAircraft.getLastVel(),
                        playerAircraft.getHeading(), playerAircraft.getPitch());
                    playerAircraft = null;
                }
                else
                {
                    // enter the closest aircraft (if there is one within range)
                    if (aircrafts.Count != 0)
                    {
                        Aircraft closest = aircrafts
                            .OrderBy(aircraft => aircraft.getLoc().distTo(playerPerson.getLoc()))
                            .FirstOrDefault();
                        if (closest != null && closest.getLoc().distTo(playerPerson.getLoc()) <= 5)
                        {
                            playerPerson = null;
                            playerAircraft = closest;
                        }
                    }
                }
            }

            if (playerAircraft != null)
            {
                float planeHeading = playerAircraft.getHeading();
                Vector3 planeLoc = playerAircraft.getLoc();
                Vector3 planeDir = Sphere.degrees(planeHeading, playerAircraft.getPitch());
                Vector3 planeUp = Sphere.degrees(planeHeading, 90 + playerAircraft.getPitch());

                Vector3 cameraTarget = planeLoc;
                cameraTarget += planeUp * playerAircraft.VIEW_Z;

                cameraLoc = cameraTarget;
                cameraLoc -= planeDir * d;

                if (cameraLoc.Z <= 0.1f) cameraLoc.Z = 0.1f;

                Vector3 cameraUp = planeUp;

                view = Matrix.CreateLookAt(
                    cameraLoc,
                    cameraTarget,
                    cameraUp);
            }
            else
            {
                float playerHeading = playerPerson.getHeading();
                float playerPitch = playerPerson.getPitch();
                Vector3 playerDir = Sphere.degrees(playerHeading, playerPitch);

                cameraLoc = playerPerson.getLoc();
                cameraLoc.Z += playerPerson.CAMERA_HEIGHT;
                Vector3 cameraTarget = cameraLoc + playerDir;

                view = Matrix.CreateLookAt(
                    cameraLoc,
                    cameraTarget,
                    new Vector3(0, 0, 1));
            }
            
            bool isBraking = Key.isPressed(Keys.Space);

            int numTimes = 1;
            if (Key.isPressed(Keys.OemTilde))
                numTimes = 10;
            while (numTimes-- > 0)
            {
                if (playerAircraft != null)
                {
                    if (controlMode == KEYBOARD_CONTROL)
                    {
                        float keyboardInput = 0.5f;

                        float yawInput = 0;
                        if (Key.isPressed(Keys.A) || Key.isPressed(Keys.Left)) yawInput -= keyboardInput;
                        if (Key.isPressed(Keys.D) || Key.isPressed(Keys.Right)) yawInput += keyboardInput;

                        float pitchInput = 0;
                        if (Key.isPressed(Keys.W) || Key.isPressed(Keys.Up)) pitchInput -= keyboardInput;
                        if (Key.isPressed(Keys.S) || Key.isPressed(Keys.Down)) pitchInput += keyboardInput;

                        playerAircraft.update((float)dt, new ControlInputs(yawInput, pitchInput, power, isBraking));
                    }
                    else // MOUSE_CONTROL
                    {
                        playerAircraft.update((float)dt, new ControlInputs(
                            lerp(Mouse.GetState().X, 0, width, -1, 1),
                            lerp(Mouse.GetState().Y, 0, height, -1, 1),
                            power, isBraking));
                    }
                }
                else
                {
                    playerPerson.update((float)dt);
                }

                foreach (Aircraft aircraft in aircrafts)
                    if (playerAircraft == null || aircraft != playerAircraft)
                        aircraft.update((float)dt);
            }

            base.Update(gameTime);
        }

        Matrix view, proj;
        Vector3 lightDir = new Vector3(0.2f, 0, -1);

        protected override void Draw(GameTime gameTime)
        {
            float dt = (float)gameTime.ElapsedGameTime.TotalSeconds;

            IsMouseVisible = playerPerson == null;

            GraphicsDevice.Clear(new Color(50, 50, 255));

            proj = Matrix.CreatePerspectiveFieldOfView(
                MathHelper.ToRadians(playerPerson == null ? 60f : playerPerson.FIELD_OF_VIEW),
                (float)width / height,
                1.0f, 100000f);

            GraphicsDevice.SamplerStates[0] = SamplerState.PointWrap;
            GraphicsDevice.DepthStencilState = DepthStencilState.Default;
            GraphicsDevice.BlendState = BlendState.AlphaBlend;
            GraphicsDevice.RasterizerState = RasterizerState.CullNone;
            
            drawQuad(dirtTileImage, Color.White,
                0, 0, 20000, 20000, // x, y, w, h
                -20000f / 50f, 20000f / 50f, -20000f / 50f, 20000f / 50f); // texture coords

            Vector3 centerLoc = playerAircraft == null ? playerPerson.getLoc() : playerAircraft.getLoc();

            //directionalLight(100, 100, 80, lightDir.x, lightDir.y, lightDir.z);

            GraphicsDevice.DepthStencilState = DepthStencilState.None;

            // airport(s)
            drawQuad(airportImage, Color.White, 0, 0, ImageMode.Corner);
            airport.drawAt(1000 / 2f, 1000 / 2f);

            foreach (Aircraft aircraft in aircrafts)
            {
                Vector3 shadowLoc = Shadow.shadowLoc(lightDir, aircraft.getLoc(), 0);
                // shadow
                drawQuad(shadowImage, Color.White, 
                    shadowLoc.X, shadowLoc.Y, 
                    shadowImage.Width / 10f, shadowImage.Height / 10f); 
            }

            GraphicsDevice.DepthStencilState = new DepthStencilState()
            {
                DepthBufferWriteEnable = true,
                TwoSidedStencilMode = true
            };

            //// control tower
            //pushMatrix();
            //{
            //    translate(-320, -225, 0);
            //    scale(10);
            //    shape(controlTowerModel);
            //}
            //popMatrix();

            // draw all aircrafts
            //GraphicsDevice.RasterizerState = new RasterizerState() { FillMode = FillMode.WireFrame, CullMode = CullMode.None };
            foreach (Aircraft aircraft in aircrafts)
                aircraft.draw();

            #region 2d stuff (text)
            // 2d stuff now
            int line = 0;
            spriteBatch.Begin();
            {
                drawText("FPS: " + Math.Round(1 / dt), 10, ++line * 20);
                if (playerAircraft != null)
                {
                    drawText("Heading: " + (360 - Math.Round(playerAircraft.getHeading())), 10, ++line * 20);
                    drawText("Pitch: " + Math.Round(playerAircraft.getPitch()), 10, ++line * 20);
                    if (playerAircraft is Airplane)
                        drawText("Velocity: " + Math.Round(((Airplane)playerAircraft).getVelocity()), 10, ++line * 20);
                    else if (playerAircraft is Helicopter)
                        drawText("Velocity: " + Math.Round(((Helicopter)playerAircraft).getVelocity()), 10, ++line * 20);
                    drawText("V-Speed: " + Math.Round(playerAircraft.getVSpeed() * 60), 10, ++line * 20);
                    drawText("Location: " + vstr(playerAircraft.getLoc()), 10, ++line * 20);
                    drawText("Altitude: FL " + ((float)Math.Round(playerAircraft.getLoc().Z / 10) / 10), 10, ++line * 20);
                    //text("Shadow-loc: " + vstr(shadowLoc), 10, ++line * 20);
                    drawText("Power Target: " + Math.Round(power * 100) + "%", 10, (line += 2) * 20);
                    if (playerAircraft is Airplane)
                        drawText("Actual Power: " + Math.Round(((Airplane)playerAircraft).getPower() / ((Airplane)playerAircraft).MAX_POWER * 100) + "%", 10, ++line * 20);
                    else if (playerAircraft is Helicopter)
                        drawText("Actual Power: " + Math.Round(((Helicopter)playerAircraft).getPower() / ((Helicopter)playerAircraft).MAX_POWER * 100) + "%", 10, ++line * 20);
                    if (playerAircraft is Airplane)
                        drawText("Lift: " + ((float)Math.Round(((Airplane)playerAircraft).calcLift() * 100) / 100), 10, (line += 2) * 20);
                    drawText("Camera Distance: " + Math.Round(d), 10, (line += 2) * 20);
                    IAutopilot autopilot = playerAircraft.getAutopilot();
                    if (autopilot is AirplaneAutopilot)
                    {
                        AirplaneAutopilot ap = (AirplaneAutopilot)autopilot;
                        drawText("Autopilot:", 10, (line += 2) * 20);
                        drawText("Altitude: " + onOff(ap.ap_alt) + " @ " + Math.Round(ap.targetAlt) + " ft", 10, (++line) * 20);
                        drawText("VSpeed: " + onOff(ap.ap_alt) + " @ " + Math.Round(ap.targetVSpeed * 60) + " ft/min", 10, (++line) * 20);
                        drawText("Speed: " + onOff(ap.ap_speed) + " @ " + Math.Round(ap.targetSpeed) + " ft/s", 10, (++line) * 20);
                        drawText("Heading: " + onOff(ap.ap_heading) + " @ " + Math.Round(ap.targetHeading), 10, (++line) * 20);
                        drawText("Current setting: " + ap_setting_str(ap_setting), 10, (line += 2) * 20);
                    }
                    else if (autopilot is HelicopterAutopilot)
                    {
                        //				HelicopterAutopilot ap = (HelicopterAutopilot)autopilot;
                        //				drawText("Autopilot:", 10, (line+=2) * 20);
                        //				drawText("Altitude: " + onOff(ap.ap_alt) + " @ " + round(ap.target_alt) + " ft", 10, (++line) * 20);
                        //				drawText("VSpeed: " + onOff(ap.ap_alt) + " @ " + round(ap.target_vspeed * 60) + " ft/min", 10, (++line) * 20);
                        //				drawText("Speed: " + onOff(ap.ap_speed) + " @ " + round(ap.target_speed) + " ft/s", 10, (++line) * 20);
                        //				drawText("Heading: " + onOff(ap.ap_heading) + " @ " + round(ap.target_heading), 10, (++line) * 20);
                        //				drawText("Current setting: " + ap_setting_str(ap_setting), 10, (line+=2) * 20);
                    }
                }

                spriteBatch.line2d(Color.Black, 1, width / 2, height / 2 - 5, width / 2, height / 2 + 5);
                spriteBatch.line2d(Color.Black, 1, width / 2 - 5, height / 2, width / 2 + 5, height / 2);
            }
            spriteBatch.End();
            #endregion

            Key.finishFrame();

            base.Draw(gameTime);
        }

        #region Drawing Methods
        void drawText(string text, float x, float y)
        {
            spriteBatch.DrawString(font, text, new Vector2(x, y), Color.White);
        }

        public void drawModel(Model model, Matrix scaling, Matrix rotation, Matrix translation)
        {
            // Copy any parent transforms.
            Matrix[] transforms = new Matrix[model.Bones.Count];
            model.CopyAbsoluteBoneTransformsTo(transforms);

            Matrix modelTransform = scaling * rotation * translation;

            // Draw the model. A model can have multiple meshes, so loop.
            foreach (ModelMesh mesh in model.Meshes)
            {
                // This is where the mesh orientation is set, as well
                // as our camera and projection.
                foreach (BasicEffect effect in mesh.Effects)
                {
                    effect.EnableDefaultLighting();
                    effect.World = transforms[mesh.ParentBone.Index] * modelTransform;
                    effect.View = view;
                    effect.Projection = proj;
                    effect.LightingEnabled = true;
                    effect.DirectionalLight0.Enabled = true;
                    effect.DirectionalLight0.Direction = lightDir;
                    effect.DirectionalLight0.DiffuseColor = new Vector3(1f) / 2f;
                    effect.AmbientLightColor = new Vector3(0.8f);
                    effect.SpecularPower = float.MaxValue;
                }
                // Draw the mesh, using the effects set above.
                mesh.Draw();
            }
        }
        public void drawModel(Model model, float scale, float heading, float pitch, float roll, Vector3 loc)
        {
            Matrix headingRotation = Matrix.CreateRotationZ(MathHelper.ToRadians(heading)),
                   pitchRotation = Matrix.CreateRotationY(MathHelper.ToRadians(-pitch)),
                   rollRotation = Matrix.CreateRotationX(MathHelper.ToRadians(roll));
            FlightGame.game.drawModel(model,
                Matrix.CreateScale(scale),
                rollRotation * pitchRotation * headingRotation,
                Matrix.CreateTranslation(loc));
        }


        BasicEffect basicEffect;
        VertexDeclaration vertexDeclaration;

        public enum ImageMode { Center, Corner }
        public void drawQuad(Texture2D texture, Color tint,
            float x, float y, float w, float h,
            ImageMode mode = ImageMode.Center)
        {
            this.drawQuad(texture, tint, x, y, w, h, 0, 1, 0, 1, mode);
        }
        public void drawQuad(Texture2D texture, Color tint, 
            float x, float y, 
            ImageMode mode = ImageMode.Center)
        {
            drawQuad(texture, tint, x, y, texture.Width, texture.Height, mode);
        }
        public void drawQuad(Texture2D texture, Color tint, float x, float y,
            float leftTC, float rightTC, float topTC, float bottomTC,
            ImageMode mode = ImageMode.Center)
        {
            drawQuad(texture, tint, x, y, texture.Width, texture.Height, leftTC, rightTC, topTC, bottomTC, mode);
        }
        public void drawQuad(Texture2D texture, Color tint,
            float x, float y, float w, float h,
            float leftTC, float rightTC, float topTC, float bottomTC,
            ImageMode mode = ImageMode.Center)
        {
            if (mode == ImageMode.Corner)
            {
                x -= w / 2;
                y -= h / 2;
            }

            Quad quad = (mode == ImageMode.Center)
                ? Quad.FromCenter(
                    new Vector3(x, y, 0),
                    new Vector3(0, 0, 1),
                    new Vector3(0, 1, 0),
                    w, h,
                    leftTC, rightTC, topTC, bottomTC)
                : Quad.FromCorner(
                    new Vector3(x, y, 0),
                    new Vector3(0, 0, 1),
                    new Vector3(0, 1, 0),
                    w, h,
                    leftTC, rightTC, topTC, bottomTC);

            basicEffect.Texture = texture;
            basicEffect.World = Matrix.Identity;
            basicEffect.View = view;
            basicEffect.Projection = proj;
            basicEffect.DiffuseColor = tint.ToVector3();

            foreach (EffectPass pass in basicEffect.CurrentTechnique.Passes)
            {
                pass.Apply();

                GraphicsDevice.DrawUserIndexedPrimitives<VertexPositionNormalTexture>(
                    PrimitiveType.TriangleList,
                    quad.Vertices, 0, 4,
                    quad.Indexes, 0, 2);
            }
        }

        public void drawQuad(Texture2D texture, Color tint,
            Matrix transform,
            float width, float height,
            float leftTC, float rightTC, float topTC, float bottomTC)
        {
            Quad quad = Quad.FromCorner(
                Vector3.Zero,
                new Vector3(0, 0, 1),
                new Vector3(0, 1, 0),
                width, height,
                leftTC, rightTC, topTC, bottomTC);

            basicEffect.Texture = texture;
            basicEffect.World = transform;
            basicEffect.View = view;
            basicEffect.Projection = proj;
            basicEffect.DiffuseColor = tint.ToVector3();

            foreach (EffectPass pass in basicEffect.CurrentTechnique.Passes)
            {
                pass.Apply();

                GraphicsDevice.DrawUserIndexedPrimitives<VertexPositionNormalTexture>(
                    PrimitiveType.TriangleList,
                    quad.Vertices, 0, 4,
                    quad.Indexes, 0, 2);
            }
        }

        public void draw(Matrix world, Texture2D texture, Color tint,
            VertexBuffer vertexBuffer, params VertexPositionNormalTexture[] vertices)
        {
            basicEffect.World = world;
            basicEffect.Texture = texture;
            basicEffect.View = view;
            basicEffect.Projection = proj;
            basicEffect.DiffuseColor = tint.ToVector3();

            foreach (EffectPass pass in basicEffect.CurrentTechnique.Passes)
            {
                pass.Apply();

                GraphicsDevice.SetVertexBuffer(vertexBuffer);
                GraphicsDevice.DrawUserPrimitives<VertexPositionNormalTexture>(PrimitiveType.TriangleStrip,
                    vertices, 0, vertices.Length - 2);
            }
        }
        #endregion

        #region Helper Methods
        double constrain(double v, double l, double h)
        {
            return (v < l) ? l : (v > h ? h : v);
        }
        float lerp(float x, float l, float r, float yl, float yr)
        {
            return (x - l) * (yr - yl) / (r - l) + yl;
        }

        void onMouseWheel(int delta)
        {
            power = MathHelper.Clamp(power + delta / 10f, 0, 1);
        }

        static char ap_setting = 'a';
        static string onOff(bool b)
        {
            return b ? "ON" : "OFF";
        }
        static String ap_setting_str(char setting)
        {
            switch (setting)
            {
                case 'a': return "altitude";
                case 'v': return "v-speed";
                case 's': return "speed";
                case 'h': return "heading";
                default: return "not a valid ap setting...";
            }
        }
        static String vstr(Vector3 vec)
        {
            return "[" + Math.Round(vec.X) + "," + Math.Round(vec.Y) + "," + Math.Round(vec.Z) + "]";
        }
        #endregion
    }
}
